Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
A tiny, fast, unopinionated handler for updating JS objects and arrays immutably
A tiny (~1.9kB minified+gzipped), fast, unopinionated handler for updating JS objects and arrays immutably.
Supports nested key paths via path arrays or dot-bracket syntax, and all methods are curriable (with placeholder support) for composability. Can be a drop-in replacement for the lodash/fp
methods get
, set
, merge
, and omit
with a 90% smaller footprint.
import {
__,
add,
assign,
call,
get,
getOr,
merge,
remove,
set,
transform
} from "unchanged";
const object = {
foo: "foo",
bar: [
{
baz: "quz"
}
]
};
// handle standard properties
const foo = get("foo", object);
// or nested properties
const baz = set("bar[0].baz", "not quz", object);
// all methods are curriable
const removeBaz = remove("bar[0].baz");
const sansBaz = removeBaz(object);
NOTE: There is no default
export, so if you want to import all methods to a single namespace you should use the import *
syntax:
import * as uc from "unchanged";
get(path: (Array<number|string>|number|string), object: (Array<any>|Object)): any
Get the value at the path
requested on the object
passed.
const object = {
foo: [
{
bar: "baz"
}
]
};
console.log(get("foo[0].bar", object)); // baz
console.log(get(["foo", 0, "bar"], object)); // baz
getOr(fallbackValue: any, path: (Array<number|string>|number|string), object: (Array<any>|Object)): any
Get the value at the path
requested on the object
passed, with a fallback value if that path does not exist.
const object = {
foo: [
{
bar: "baz"
}
]
};
console.log(getOr("blah", "foo[0].bar", object)); // baz
console.log(getOr("blah", ["foo", 0, "bar"], object)); // baz
console.log(getOr("blah", "foo[0].nonexistent", object)); // blah
set(path: (Array<number|string>|number|string), value: any, object: (Array<any>|object)): (Array<any>|Object)
Returns a new clone of the object
passed, with the value
assigned to the final key on the path
specified.
const object = {
foo: [
{
bar: "baz"
}
]
};
console.log(set("foo[0].bar", "quz", object)); // {foo: [{bar: 'quz'}]}
console.log(set(["foo", 0, "bar"], "quz", object)); // {foo: [{bar: 'quz'}]}
remove(path: (Array<number|string>|number|string), object: (Array<any>|object)): (Array<any>|Object)
Returns a new clone of the object
passed, with the final key on the path
removed if it exists.
const object = {
foo: [
{
bar: "baz"
}
]
};
console.log(remove("foo[0].bar", object)); // {foo: [{}]}
console.log(remove(["foo", 0, "bar"], object)); // {foo: [{}]}
has(path: (Array<number|string>|number|string), object: (Array<any>|object)): boolean
Returns true
if the object has the path provided, false
otherwise.
const object = {
foo: [
{
bar: "baz"
}
]
};
console.log(has("foo[0].bar", object)); // true
console.log(has(["foo", 0, "bar"], object)); // true
console.log(has("bar", object)); // false
add(path: (Array<number|string>|number|string), value: any, object: (Array<any>|object)): (Array<any>|Object)
Returns a new clone of the object
passed, with the value
added at the path
specified. This can have different behavior depending on whether the item is an Object
or an Array
.
const object = {
foo: [
{
bar: 'baz'
}
]
};
// object
console.log(add('foo', 'added value' object)); // {foo: [{bar: 'baz'}, 'added value']}
console.log(add(['foo'], 'added value', object)); // {foo: [{bar: 'baz'}, 'added value']}
// array
console.log(add('foo[0].quz', 'added value' object)); // {foo: [{bar: 'baz', quz: 'added value'}]}
console.log(add(['foo', 0, 'quz'], 'added value', object)); // {foo: [{bar: 'baz', quz: 'added value'}]}
Notice that the Object
usage is idential to the set
method, where a key needs to be specified for assignment. In the case of an Array
, however, the value is pushed to the array at that key.
NOTE: If you want to add an item to a top-level array, pass null
as the key:
const object = ["foo"];
console.log(add(null, "bar", object)); // ['foo', 'bar']
merge(path: (Array<number|string>|number|string), value: any, object: (Array<any>|object)): (Array<any>|Object)
Returns a new object that is a deep merge of value
into object
at the path
specified. If you want to perform a shallow merge, see assign
.
const object1 = {
oneSpecific: "value",
object: {
one: "value1",
two: "value2"
}
};
const object2 = {
one: "new value",
three: "value3"
};
console.log(merge("object", object2, object1));
/*
{
oneSpecific: 'value',
object: {
one: 'value1',
deeply: {
nested: 'other value',
untouched: true,
},
two: 'value2',
three: 'value3
}
}
*/
NOTE: If you want to merge
the entirety of both objects, pass null
as the key:
const object1 = {
oneSpecific: "value",
object: {
one: "value1",
deeply: {
nested: "value",
untouched: true
},
two: "value2"
}
};
const object2 = {
one: "new value",
deeply: {
nested: "other value"
},
three: "value3"
};
console.log(merge(null, object2, object1));
/*
{
one: 'new value',
oneSpecific: 'value',
object: {
one: 'value1',
deeply: {
nested: 'value',
untouched: true,
},
two: 'value2',
},
deeply: {
nested: 'other value',
},
three: 'value3
}
*/
assign(path: (Array<number|string>|number|string), value: any, object: (Array<any>|object)): (Array<any>|Object)
Returns a new object that is a shallow merge of value
into object
at the path
specified. If you want to perform a deep merge, see merge
.
const object1 = {
oneSpecific: "value",
object: {
one: "value1",
deeply: {
nested: "value",
untouched: false
},
two: "value2"
}
};
const object2 = {
one: "new value",
deeply: {
nested: "other value"
},
three: "value3"
};
console.log(assign("object", object2, object1));
/*
{
oneSpecific: 'value',
object: {
one: 'value1',
deeply: {
nested: 'other value',
},
two: 'value2',
three: 'value3
}
}
*/
NOTE: If you want to assign
the entirety of both objects, pass null
as the key:
const object1 = {
oneSpecific: "value",
object: {
one: "value1",
deeply: {
nested: "value",
untouched: true
},
two: "value2"
}
};
const object2 = {
one: "new value",
deeply: {
nested: "other value"
},
three: "value3"
};
console.log(assign(null, object2, object1));
/*
{
one: 'new value',
oneSpecific: 'value',
object: {
one: 'value1',
deeply: {
nested: 'value',
untouched: true,
},
two: 'value2',
},
deeply: {
nested: 'other value',
},
three: 'value3
}
*/
call(path: (Array<number|string>|number|string), parameters: Array<any>, object: (Array<any>|Object)[, context: any])
Call the method at the path
requested on the object
passed, and return what it's call returns.
const object = {
foo: [
{
bar(a, b) {
return a + b;
}
}
]
};
console.log(call("foo[0].bar", [1, 2], object)); // 3
console.log(call(["foo", 0, "bar"], [1, 2], object)); // 3
You can also provide an optional fourth parameter of context
, which will be the this
value in the method call. This will default to the object
itself.
const object = {
calculate: true,
foo: [
{
bar(a, b) {
return this.calculate ? a + b : 0;
}
}
]
};
console.log(call("foo[0].bar", [1, 2], object)); // 3
console.log(call("foo[0].bar", [1, 2], object, {})); // 0
NOTE: Because context
is an optional parameter, it cannot be independently curried; you must apply it in the call when the object
is passed.
transform(path: (Array<number|string>|number|string), fn: function, object: (Array<any>|object)[, ...extraParams: Array<any>]): (Array<any>|Object)
Returns a new clone of the object
passed, with the return value of fn
assigned to the final key on the path
specified. fn
is called with the current value at the path
as the first parameter, and any additional parameters passed as extraParams
following that.
const object = {
foo: [
{
bar: "baz"
}
]
};
const fn = (currentValue, preventUpdate) =>
preventUpdate ? currentValue : "quz";
console.log(transform("foo[0].bar", fn, object)); // {foo: [{bar: 'quz'}]}
console.log(transform("foo[0].bar", fn, object, true)); // {foo: [{bar: 'baz'}]}
console.log(transform(["foo", 0, "bar"], fn, object)); // {foo: [{bar: 'quz'}]}
console.log(transform(["foo", 0, "bar"], fn, object, true)); // {foo: [{bar: 'baz'}]}
NOTE: Because extraParams
are optional parameters, they cannot be independently curried; you must apply them in the call when the object
is passed.
A placeholder value used to identify "gaps" in a curried function, allowing for earlier application of arguments later in the argument order.
import {__, set} from 'unchanged';
const thing = {
foo: 'foo';
};
const setFoo = set('foo', __, thing);
setFooOnThing('bar');
lodash/fp
(the functional programming implementation of lodash
) is identical in implementation to unchanged
's methods, just with a 10.5x larger footprint. These methods should map directly:
curry.placeholder
=> __
get
=> get
getOr
=> getOr
merge
=> merge
omit
=> remove
set
=> set
(also maps to add
for objects only)NOTE: There is no direct parallel for the add
method in lodash/fp
; the closest is concat
but that is array-specific and does not support nested keys.
ramda
is similar in its implementation, however the first big difference is that dot-bracket syntax is not supported by ramda
, only path arrays. Another difference is that the ramda
methods that clone objects (assocPath
, for example) only work with objects; arrays are implicitly converted into objects, which can make updating collections challenging.
The last main difference is the way that objects are copied, example:
function Foo(value) {
this.value = value;
}
Foo.prototype.getValue = function() {
return this.value;
};
const foo = new Foo("foo");
// in ramda, both own properties and prototypical methods are copied to the new object as own properties
const ramdaResult = assoc("bar", "baz", foo);
console.log(ramdaResult); // {value: 'foo', bar: 'baz', getValue: function getValue() { return this.value; }}
console.log(ramdaResult instanceof Foo); // false
// in unchanged, the prototype of the original object is maintained, and only own properties are copied as own properties
const unchangedResult = set("bar", "baz", foo);
console.log(unchangedResult); // {value: 'foo', bar: 'baz'}
console.log(unchangedResult instanceof Foo); // true
This can make ramda
more performant in certain scenarios, but at the cost of having potentially unexpected behavior.
This includes popular solutions like Immutable.js, seamless-immutable, mori, etc. These solutions all work well, but with one caveat: you need to buy completely into their system. Each of these libraries redefines how the objects are stored internally, and require that you learn a new, highly specific API to use these custom objects. unchanged
is unopinionated, accepting standard JS objects and returning standard JS objects, no transformation or learning curve required.
Standard stuff, clone the repo and npm install
dependencies. The npm scripts available:
build
=> run webpack to build development dist
file with NODE_ENV=developmentbuild:minified
=> run webpack to build production dist
file with NODE_ENV=productiondev
=> run webpack dev server to run example app / playgrounddist
=> runs build
and build:minified
lint
=> run ESLint against all files in the src
folderprepublish
=> runs prepublish:compile
when publishingprepublish:compile
=> run lint
, test:coverage
, transpile:es
, transpile:lib
, dist
test
=> run AVA test functions with NODE_ENV=test
test:coverage
=> run test
but with nyc
for coverage checkertest:watch
=> run test
, but with persistent watchertranspile:lib
=> run babel against all files in src
to create files in lib
transpile:es
=> run babel against all files in src
to create files in es
, preserving ES2015 modules (for
pkg.module
)1.5.2
rollup
build setup with latest curriable
and pathington
setupsFAQs
A tiny, fast, unopinionated handler for updating JS objects and arrays immutably
The npm package unchanged receives a total of 17,908 weekly downloads. As such, unchanged popularity was classified as popular.
We found that unchanged demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.